package cn.snmp.mutual.uk.util;

import java.util.*;
import java.security.*;

import cn.snmp.mutual.org.bouncycastle.crypto.digests.GeneralDigest;
import cn.snmp.mutual.org.bouncycastle.crypto.digests.MD5Digest;
import cn.snmp.mutual.org.bouncycastle.crypto.digests.SHA1Digest;
import cn.snmp.mutual.org.bouncycastle.crypto.engines.AESEngine;
import cn.snmp.mutual.org.bouncycastle.crypto.engines.DESEngine;
import cn.snmp.mutual.org.bouncycastle.crypto.params.DESParameters;
import cn.snmp.mutual.org.bouncycastle.crypto.params.KeyParameter;
import cn.snmp.mutual.uk.stack.AsnObject;
import cn.snmp.mutual.uk.stack.DecodingException;
import cn.snmp.mutual.uk.stack.EncodingException;
import cn.snmp.mutual.uk.stack.PduException;

public class SnmpUtilities {
    private static final String version_id = "@(#)$Id: SnmpUtilities.java,v 1.27 2009/03/05 12:57:57 birgita Exp $ Copyright Westhawk Ltd";
    static final int ONEMEG = 1048576;
    static final int SALT_LENGTH = 8;
    private static int salt_count;
    private static long asalt;
    static final char[] HEX_DIGIT;

    public static String getSnmpVersionString(final int version) {
        String versionString = null;
        switch (version) {
            case 0: {
                versionString = "SNMPv1";
                break;
            }
            case 1: {
                versionString = "SNMPv2c";
                break;
            }
            case 3: {
                versionString = "SNMPv3";
                break;
            }
            default: {
                versionString = "Unsupported version no " + version;
                break;
            }
        }
        return versionString;
    }

    public static byte[] toBytes(String hexStr) {
        final byte mask = 127;
        byte[] bytes = new byte[0];
        if (hexStr != null) {
            hexStr = hexStr.toUpperCase();
            final int len = hexStr.length();
            bytes = new byte[len / 2];
            for (int sPos = 0, bPos = 0; sPos < len; sPos += 2, ++bPos) {
                final char a = hexStr.charAt(sPos);
                final char b = hexStr.charAt(sPos + 1);
                final int v1 = Character.digit(a, 16);
                final int v2 = Character.digit(b, 16);
                final int v3 = v1 * 16 + v2;
                bytes[bPos] = (byte) v3;
            }
        }
        return bytes;
    }




    public static void dumpBytes(final String headerStr, final byte[] bytes) {
        final StringBuffer buf = new StringBuffer(bytes.length);
        buf.append("\n");
        buf.append(headerStr).append("\n");
        buf.append("bytes.length: ").append(bytes.length).append("\n");
        int len;
        int i;
        for (len = bytes.length, i = 0, i = 0; i < len; ++i) {
            buf.append(toHex(bytes[i]) + " ");
            if (0 == (i + 1) % 8) {
                buf.append("\n");
            }
        }
        buf.append("\n");
        System.out.println(buf.toString());
    }

    public static String toHexString(final byte[] bytes) {
        String str = "";
        if (bytes != null) {
            for (int len = bytes.length, i = 0; i < len; ++i) {
                str += toHex(bytes[i]);
            }
        }
        return str;
    }

    public static String toHex(final int val) {
        final int val2 = val >> 4 & 0xF;
        final int val3 = val & 0xF;
        return "" + SnmpUtilities.HEX_DIGIT[val2] + SnmpUtilities.HEX_DIGIT[val3];
    }

    public static boolean areBytesEqual(final byte[] array1, final byte[] array2) {
        boolean same = true;
        final int len1 = array1.length;
        if (len1 == array2.length) {
            for (int i = 0; i < len1 && same; same = (array1[i] == array2[i]), ++i) {
            }
        } else {
            same = false;
        }
        return same;
    }

    public static byte[] getLocalizedKeyMD5(final byte[] passwKey, final String engineId) {
        byte[] ret = null;
        final MD5Digest mdc = new MD5Digest();
        mdc.reset();
        final byte[] beid = toBytes(engineId);
        if (beid != null && passwKey != null) {
            mdc.update(passwKey, 0, passwKey.length);
            mdc.update(beid, 0, beid.length);
            mdc.update(passwKey, 0, passwKey.length);
            ret = new byte[mdc.getDigestSize()];
            mdc.doFinal(ret, 0);
        }
        return ret;
    }

    public static byte[] getLocalizedKeySHA1(final byte[] passwKey, final String engineId) {
        byte[] ret = null;
        final SHA1Digest mdc = new SHA1Digest();
        mdc.reset();
        final byte[] beid = toBytes(engineId);
        if (beid != null && passwKey != null) {
            mdc.update(passwKey, 0, passwKey.length);
            mdc.update(beid, 0, beid.length);
            mdc.update(passwKey, 0, passwKey.length);
            ret = new byte[mdc.getDigestSize()];
            mdc.doFinal(ret, 0);
        }
        return ret;
    }

    public static byte[] passwordToKeySHA1(final String password) {
        byte[] ret = null;
        final SHA1Digest sha = new SHA1Digest();
        final byte[] passwordBuf = new byte[64];
        final int pl = password.length();
        final byte[] pass = new byte[pl];
        for (int i = 0; i < pl; ++i) {
            pass[i] = (byte) ('\u00ff' & password.charAt(i));
        }
        int count = 0;
        int passwordIndex = 0;
        final Date then = (AsnObject.debug > 1) ? new Date() : null;
        synchronized (sha) {
            while (count < 1048576) {
                int len;
                for (int cp = 0, j = 0; j < 64; j += len, cp += len, passwordIndex += len) {
                    final int pim = passwordIndex % pl;
                    len = 64 - cp;
                    final int pr = pl - pim;
                    if (len > pr) {
                        len = pr;
                    }
                    System.arraycopy(pass, pim, passwordBuf, cp, len);
                }
                sha.update(passwordBuf, 0, passwordBuf.length);
                count += 64;
            }
            ret = new byte[sha.getDigestSize()];
            sha.doFinal(ret, 0);
        }
        if (AsnObject.debug > 1) {
            final Date now = new Date();
            final long diff = now.getTime() - then.getTime();
            System.out.println("(Complex) pass to key takes " + diff / 1000.0);
        }
        return ret;
    }

    public static byte[] passwordToKeyMD5(final String password) {
        byte[] ret = null;
        final MD5Digest mdc = new MD5Digest();
        final byte[] passwordBuf = new byte[64];
        final int pl = password.length();
        final byte[] pass = new byte[pl];
        for (int i = 0; i < pl; ++i) {
            pass[i] = (byte) ('\u00ff' & password.charAt(i));
        }
        int count = 0;
        int passwordIndex = 0;
        final Date then = (AsnObject.debug > 1) ? new Date() : null;
        synchronized (mdc) {
            while (count < 1048576) {
                int len;
                for (int cp = 0, j = 0; j < 64; j += len, cp += len, passwordIndex += len) {
                    final int pim = passwordIndex % pl;
                    len = 64 - cp;
                    final int pr = pl - pim;
                    if (len > pr) {
                        len = pr;
                    }
                    System.arraycopy(pass, pim, passwordBuf, cp, len);
                }
                mdc.update(passwordBuf, 0, passwordBuf.length);
                count += 64;
            }
            ret = new byte[mdc.getDigestSize()];
            mdc.doFinal(ret, 0);
        }
        if (AsnObject.debug > 1) {
            final Date now = new Date();
            final long diff = now.getTime() - then.getTime();
            System.out.println("(Complex) pass to key takes " + diff / 1000.0);
        }
        return ret;
    }

    public static final byte[] getFingerPrintMD5(final byte[] key, final byte[] message) {
        if (AsnObject.debug > 5 && key.length != 16) {
            System.out.println("MD5 key length wrong");
        }
        return getFingerPrint(key, message, false);
    }

    public static final byte[] getFingerPrintSHA1(final byte[] key, final byte[] message) {
        if (AsnObject.debug > 5 && key.length != 20) {
            System.out.println("SHA1 key length wrong");
        }
        return getFingerPrint(key, message, true);
    }

    public static final byte[] getSaltDES(final int snmpEngineBoots) {
        if (SnmpUtilities.salt_count == -1) {
            final Random rand = new Random();
            SnmpUtilities.salt_count = rand.nextInt();
        }
        final byte[] salt = new byte[8];
        setBytesFromInt(salt, snmpEngineBoots, 0);
        setBytesFromInt(salt, SnmpUtilities.salt_count, 4);
        ++SnmpUtilities.salt_count;
        return salt;
    }

    public static byte[] getSaltAES() {
        if (SnmpUtilities.asalt == 0L) {
            final SecureRandom rand = new SecureRandom();
            SnmpUtilities.asalt = rand.nextLong();
        } else {
            ++SnmpUtilities.asalt;
        }
        final byte[] tsalt = new byte[8];
        setBytesFromLong(tsalt, SnmpUtilities.asalt, 0);
        return tsalt;
    }

    public static final byte[] getDESKey(final byte[] secretPrivacyKey) throws PduException {
        final byte[] desKey = new byte[8];
        if (secretPrivacyKey.length < 16) {
            throw new PduException("SnmpUtilities.getDESKey(): secretPrivacyKey is < 16");
        }
        System.arraycopy(secretPrivacyKey, 0, desKey, 0, 8);
        return desKey;
    }

    public static final byte[] getAESKey(final byte[] secretPrivacyKey) throws PduException {
        final byte[] aesKey = new byte[16];
        if (secretPrivacyKey.length < 16) {
            throw new PduException("SnmpUtilities.getAESKey(): secretPrivacyKey is < 16");
        }
        System.arraycopy(secretPrivacyKey, 0, aesKey, 0, 16);
        return aesKey;
    }

    public static final byte[] getDESInitialValue(final byte[] secretPrivacyKey, final byte[] salt) throws PduException {
        final byte[] initV = new byte[8];
        if (secretPrivacyKey.length < 16) {
            throw new PduException("SnmpUtilities.getInitialValue(): secretPrivacyKey is < 16");
        }
        int spk = 8;
        for (int i = 0; i < initV.length; ++i) {
            initV[i] = (byte) (secretPrivacyKey[spk] ^ salt[i]);
            ++spk;
        }
        return initV;
    }

    public static byte[] getAESInitialValue(final int engineBoots, final int engineTime, final byte[] salt) {
        final byte[] ret = new byte[16];
        setBytesFromInt(ret, engineBoots, 0);
        setBytesFromInt(ret, engineTime, 4);
        ret[8] = salt[0];
        ret[9] = salt[1];
        ret[10] = salt[2];
        ret[11] = salt[3];
        ret[12] = salt[4];
        ret[13] = salt[5];
        ret[14] = salt[6];
        ret[15] = salt[7];
        return ret;
    }

    public static byte[] AESencrypt(final byte[] plaintext, final byte[] secretPrivacyKey, final int engineBoots, final int engineTime, final byte[] salt) throws EncodingException {
        byte[] aesKey = null;
        byte[] iv = null;
        try {
            aesKey = getAESKey(secretPrivacyKey);
            iv = getAESInitialValue(engineBoots, engineTime, salt);
        } catch (PduException exc) {
            throw new EncodingException(exc.getMessage());
        }
        final AESEngine aes = new AESEngine();
        final KeyParameter param = new KeyParameter(aesKey);
        aes.init(true, param);
        final int newL = plaintext.length;
        final int bcount = newL / 16;
        final byte[] result = new byte[newL];
        final byte[] in = new byte[16];
        final byte[] out = new byte[16];
        int posIn = 0;
        int posResult = 0;
        System.arraycopy(iv, 0, in, 0, 16);
        for (int b = 0; b < bcount; ++b) {
            aes.processBlock(in, 0, out, 0);
            for (int i = 0; i < 16; ++i) {
                in[i] = (result[posResult] = (byte) (out[i] ^ plaintext[posIn]));
                ++posResult;
                ++posIn;
            }
        }
        if (posIn < newL) {
            aes.processBlock(in, 0, out, 0);
            for (int j = 0; posIn < newL; ++posIn, ++j) {
                result[posResult] = (byte) (out[j] ^ plaintext[posIn]);
                ++posResult;
            }
        }
        return result;
    }

    public static final byte[] DESencrypt(final byte[] plain, final byte[] secretPrivacyKey, final byte[] salt) throws EncodingException {
        byte[] desKey = null;
        byte[] iv = null;
        try {
            desKey = getDESKey(secretPrivacyKey);
            iv = getDESInitialValue(secretPrivacyKey, salt);
        } catch (PduException exc) {
            throw new EncodingException(exc.getMessage());
        }
        final int l = plain.length;
        int div = l / 8;
        final int mod = l % 8;
        if (mod > 0) {
            ++div;
        }
        final int newL = div * 8;
        final byte[] paddedOrig = new byte[newL];
        System.arraycopy(plain, 0, paddedOrig, 0, l);
        for (int i = l; i < newL; ++i) {
            paddedOrig[i] = 0;
        }
        final DESEngine des = new DESEngine();
        final DESParameters param = new DESParameters(desKey);
        des.init(true, param);
        final byte[] result = new byte[newL];
        final byte[] in = new byte[8];
        final byte[] cipherText = iv;
        int posIn = 0;
        int posResult = 0;
        for (int b = 0; b < div; ++b) {
            for (int j = 0; j < 8; ++j) {
                in[j] = (byte) (paddedOrig[posIn] ^ cipherText[j]);
                ++posIn;
            }
            des.processBlock(in, 0, cipherText, 0);
            System.arraycopy(cipherText, 0, result, posResult, cipherText.length);
            posResult += cipherText.length;
        }
        return result;
    }

    public static final byte[] DESdecrypt(final byte[] encryptedText, final byte[] salt, final byte[] secretPrivacyKey) throws DecodingException {
        final int l = encryptedText.length;
        final int div = l / 8;
        final int mod = l % 8;
        if (mod != 0) {
            throw new DecodingException("SnmpUtilities.decrypt(): The encrypted scoped PDU should be a multiple of 8 bytes");
        }
        byte[] desKey = null;
        byte[] iv = null;
        try {
            desKey = getDESKey(secretPrivacyKey);
            iv = getDESInitialValue(secretPrivacyKey, salt);
        } catch (PduException exc) {
            throw new DecodingException(exc.getMessage());
        }
        final DESEngine des = new DESEngine();
        final DESParameters param = new DESParameters(desKey);
        des.init(false, param);
        final byte[] plain = new byte[l];
        final byte[] in = new byte[8];
        final byte[] out = new byte[8];
        final byte[] cipherText = iv;
        int posPlain = 0;
        int posEncr = 0;
        for (int b = 0; b < div; ++b) {
            System.arraycopy(encryptedText, posEncr, in, 0, in.length);
            posEncr += in.length;
            des.processBlock(in, 0, out, 0);
            for (int i = 0; i < 8; ++i) {
                plain[posPlain] = (byte) (out[i] ^ cipherText[i]);
                ++posPlain;
            }
            System.arraycopy(in, 0, cipherText, 0, in.length);
        }
        return plain;
    }

    public static final byte[] AESdecrypt(final byte[] ciphertext, final byte[] secretPrivacyKey, final int engineBoots, final int engineTime, final byte[] salt) throws DecodingException {
        byte[] aesKey = null;
        byte[] iv = null;
        try {
            aesKey = getAESKey(secretPrivacyKey);
            iv = getAESInitialValue(engineBoots, engineTime, salt);
        } catch (PduException exc) {
            throw new DecodingException(exc.getMessage());
        }
        final AESEngine aes = new AESEngine();
        final KeyParameter param = new KeyParameter(aesKey);
        aes.init(true, param);
        final int newL = ciphertext.length;
        final int bcount = newL / 16;
        final byte[] result = new byte[newL];
        final byte[] in = new byte[16];
        final byte[] out = new byte[16];
        int posIn = 0;
        int posResult = 0;
        System.arraycopy(iv, 0, in, 0, 16);
        for (int b = 0; b < bcount; ++b) {
            aes.processBlock(in, 0, out, 0);
            for (int i = 0; i < 16; ++i) {
                result[posResult] = (byte) (out[i] ^ ciphertext[posIn]);
                in[i] = ciphertext[posIn];
                ++posResult;
                ++posIn;
            }
        }
        if (posIn < newL) {
            aes.processBlock(in, 0, out, 0);
            for (int j = 0; posIn < newL; ++posIn, ++j) {
                result[posResult] = (byte) (out[j] ^ ciphertext[posIn]);
                ++posResult;
            }
        }
        return result;
    }

    static byte[] getFingerPrint(final byte[] key, final byte[] message, final boolean doSha) {
        final byte[] k1 = new byte[64];
        final byte[] k2 = new byte[64];
        final byte z1 = 54;
        final byte z2 = 92;
        int kl;
        int i;
        for (kl = key.length, i = 0; i < kl; ++i) {
            k1[i] = (byte) (ifb(key[i]) ^ 0x36);
            k2[i] = (byte) (ifb(key[i]) ^ 0x5C);
        }
        while (i < 64) {
            k1[i] = z1;
            k2[i] = z2;
            ++i;
        }
        byte[] interm = null;
        final GeneralDigest mdc = doSha ? new SHA1Digest() : new MD5Digest();
        mdc.reset();
        mdc.update(k1, 0, k1.length);
        mdc.update(message, 0, message.length);
        interm = new byte[mdc.getDigestSize()];
        mdc.doFinal(interm, 0);
        byte[] rettmp = null;
        final GeneralDigest mdc2 = doSha ? new SHA1Digest() : new MD5Digest();
        mdc2.reset();
        mdc2.update(k2, 0, k2.length);
        mdc2.update(interm, 0, interm.length);
        rettmp = new byte[mdc2.getDigestSize()];
        mdc2.doFinal(rettmp, 0);
        byte[] ret = null;
        if (rettmp != null) {
            ret = new byte[12];
            System.arraycopy(rettmp, 0, ret, 0, 12);
        }
        return ret;
    }

    static final int ifb(final byte b) {
        return intFromByteWithoutStupidJavaSignExtension(b);
    }

    static final int intFromByteWithoutStupidJavaSignExtension(final byte val) {
        int ret = 0x7F & val;
        if (val < 0) {
            ret += 128;
        }
        return ret;
    }

    static final void setBytesFromInt(final byte[] ret, final int value, final int offs) {
        final int v = value;
        int j = offs;
        ret[j++] = (byte) (v >>> 24 & 0xFF);
        ret[j++] = (byte) (v >>> 16 & 0xFF);
        ret[j++] = (byte) (v >>> 8 & 0xFF);
        ret[j++] = (byte) (v >>> 0 & 0xFF);
    }

    static final void setBytesFromLong(final byte[] ret, final long value, final int offs) {
        final long v = value;
        int j = offs;
        ret[j++] = (byte) (v >>> 56 & 0xFFL);
        ret[j++] = (byte) (v >>> 48 & 0xFFL);
        ret[j++] = (byte) (v >>> 40 & 0xFFL);
        ret[j++] = (byte) (v >>> 32 & 0xFFL);
        ret[j++] = (byte) (v >>> 24 & 0xFFL);
        ret[j++] = (byte) (v >>> 16 & 0xFFL);
        ret[j++] = (byte) (v >>> 8 & 0xFFL);
        ret[j++] = (byte) (v >>> 0 & 0xFFL);
    }

    static {
        SnmpUtilities.salt_count = -1;
        HEX_DIGIT = new char[]{'0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'A', 'B', 'C', 'D', 'E', 'F'};
    }
}
